<?php

require_once 'RequestHandler.php';
require_once 'BackupManager.php';

function handle_update ()
{
	global $RH_ATF;
	static $rh;

	if (!isset($rh)) {
		$_base_dir = url_dirname(__FILE__);
		$config = $GLOBALS['config']; // copy global config array
		require_once $_base_dir . 'config.php';
		$rh = new UpdateRequestHandler($config);
		$RH_ATF['Backup']['go'] = $_base_dir.'backup.php';
	}

	if ($rh->init()) return call_user_func($rh->dispatch());

	return;

	// забекапить бд, применить апдейт структуры бд

	// найти патчи
	// создать список патчей, в порядке установки
	//
	// для каждого патча
	//
	// создать спецпапку, скопировать туда код, применить апдейт кода
	// перестроить конфигурационный файл в спец.папке
	// спросить недостающие поля файла конфигурации
	// сделать вид, что корень платоники - в спецпапке (.htaccess)
	// запустить тест на новой версии
	//        o если ок - вылить спецпапку на место сайта, убить бекап бд, зачистить кеш темплейтера
	//        o если не ок - сказать что именно не ок, восстановить бд

	# сформировать идентификатор узла (см.#436) -- если его ещё нет

	// убить спецпапку

} // end of function handle_check

class UpdateRequestHandler extends RequestHandler
{
	public function _init ()
	{
		$this->tpl = tpl_engine();
		$this->tpl->tpl_dirs[] = url_dirname(__FILE__);

		$this->usm = new UpdateSiteManipulator($this->usm);

		$this->initialState = 'Inactive';
		// action transition function
		$this->atf = array(
			'Inactive'      => array(
				'refresh'           => array($this, 'goFindPatch'),
				'cancel'            => array($this, 'goOpenSite'),
			),
			'PatchFound'    => array(
				'install'           => array($this, 'goPreparePatchInstall'),
			),
			'BackupDB'      => array(
				'install'           => array($this, 'goBackupDB'),
				'cancel'            => array($this, 'goRestoreDB'),
			),
			'ConfigureOrInstall' => array(
				'install'           => array($this, 'goConfigureOrInstall'),
			),
			'Configure'     => array(
				'form'              => array($this, 'goShowPatchForm'),
			),
			'Patch'         => array(
				'install'           => array($this, 'goPatch'),
			),
			'PostPatch'         => array(
				'install'           => array($this, 'goPostPatch'),
			),
			'ShowSite'      => array(
				'refresh'           => array($this, 'goShowSite'),
				'install'           => array($this, 'goInstall'),
				'cancel'            => array($this, 'goOpenSite'),
			),
			'InternalError' => array(
				'*'                 => array($this, 'goOpenSite'),
			),
			'_undefined'    => array(
				'*'                 => array($this, 'goInitialState'),
			),
		);
		if (!isset($this->ttm)) return false;
		return true;
	} // end of method _init

	public function handle ()
	{
		// handle automata
		$event = $_GET['event'];
		$this->currentState = isset($_GET['state']) ? $_GET['state'] : $this->initialState;

		$atf = isset($this->atf[$this->currentState][$event])
			? $this->atf[$this->currentState][$event]
			: array($this, 'goUnexpectedEvent');
		assert('is_callable($atf)');
		$nextState = call_user_func($atf, $event);
		if (!$nextState) $nextState = $this->currentState;
		if (!isset($this->atf[$nextState])) { /* 404? */ $nextState = $this->undefinedState($event, $nextState); }
		$this->currentState = $nextState;

		// handle state render
		$sv1 = $this->currentState.'!'.$event;
		$sv2 = $this->currentState;
		$sv3 = '_undefined';
		if     (isset($this->ttm[$sv1])) $stateView = $this->ttm[$sv1];
		elseif (isset($this->ttm[$sv2])) $stateView = $this->ttm[$sv2];
		else                             $stateView = $this->ttm[$sv3];
		$this->tpl->set('rh', $this);
		$this->tpl->set('states', $this->atf[$this->currentState]);
		$this->tpl->parse($stateView, 'body');
	} // end of method handle
	public function undefinedState ($event, $state)
	{
		assert("0 ;// $state");
		return $this->initialState;
	} // end of method undefinedState
	public function doActionTransition ($anotherState, $anotherEventType, $event)
	{
		assert('isset($this->atf[$anotherState][$anotherEventType])');
		assert('$this->atf[$anotherState][$anotherEventType]');
		return call_user_func($this->atf[$anotherState][$anotherEventType], $event);
	} // end of method doActionTransition
	public function doActionTransition1 ($anotherState, $anotherEventType, $event)
	{
		assert('isset($this->atf[$anotherState][$anotherEventType])');
		assert('$this->atf[$anotherState][$anotherEventType]');
		$url = '?'.http_build_query(array('m'=>'update','state'=>$anotherState, 'event'=>$anotherEventType));
		request_redirect($url);
		die();
	} // end of method doActionTransition
	public function goUnexpectedEvent ($event)
	{
		assert("0 ;// unexpectd event [$event]");
		//return $this->initialState;
		return $this->doActionTransition($this->initialState, 'refresh', $event);
	} // end of method unexpectedEvent

	public function goFindPatch ($event)
	{
		// в конфиге смотрим текущую версию сайта
		// смотрим список патчей
		// находим следующий по версии

		if ($this->usm->init() && $this->usm->findPatch()) {
			if (isset($this->usm->patch)) return 'PatchFound';
			return $this->currentState;
		} else {
			$this->error = $this->usm->error;
			return 'InternalError';
		}
	} // end of method findPatch
	public function goPreparePatchInstall ($event)
	{
		if ($this->usm->init()
			&& $this->usm->findPatch()
				&& $this->usm->copySite()
					&& $this->usm->closeSite()
						&& $this->usm->applyPatchFilesTemporarySite()
							) {
								return $this->doActionTransition1('BackupDB', $event, $event);
							}
		$this->error = $this->usm->error;
		return 'InternalError';
	} // end of method goPreparePatchInstall
	public function goConfigureOrInstall ($event)
	{
		if ($this->usm->init() && $this->usm->findPatch()) {
			assert('is_object($this->usm->patch)');
			if (isset($this->usm->patch->form)) {
				return $this->doActionTransition1('Configure', 'form', $event);
			}
			return $this->doActionTransition1('Patch', 'install', $event);
		}
		$this->error = $this->usm->error;
		return 'InternalError';
	} // end of method goPreparePatchInstall
	public function goBackupDB ($event)
	{
		if ($this->usm->init() && $this->usm->findPatch()) {
			global $RH_NEXT_STATE, $RH_ERROR;
			ob_start();
			if ($RH_ERROR) {
				$this->error = $RH_ERROR;
				ob_end_clean();
				$this->usm->deleteBackup();
				return 'InternalError';
			}
			if (!file_exists($this->usm->getBackup()->getPath())) {
				$RH_NEXT_STATE = 'Backup';
				$db_php = $this->usm->base_dir . 'etc/conf.d/db.php';
				require $db_php;
				assert('is_array($config) ;// database config not found in $config');
				assert('$config["db_host"] && $config["db_name"] && $config["db_user"] && $config["db_password"]');
				$GLOBALS['M_ADMIN_UPDATE'] = array(
					'config' => $config,
					'dump_sql' => $this->usm->getBackup()->getPath(),
				);
				$this->silent = true;
				// HACK : lucky : 2008-03-14
				// запрещаем вывод текущей вьюхи..
				// после того, как бекап отработает
				return $this->currentState;
			}
			return $this->doActionTransition('ConfigureOrInstall', $event, $event);
		}
		$this->error = $this->usm->error;
		return 'InternalError';
	} // end of method goPreparePatchInstall
	public function goRestoreDB ($event)
	{
		return 'NotImplemented';
	} // end of method goPreparePatchInstall
	public function goShowPatchForm ($event)
	{
		assert('$this->usm->init()');

		if ($this->usm->init() && $this->usm->findPatch()) {
			assert('is_object($this->usm->patch)');
			$patch = $this->usm->patch;
			assert('file_exists($patch->index_php)');
			require_once $patch->index_php;
			if (function_exists('handle_update_configure')) {
				handle_update_configure($patch);
			} else {
				require_once 'FormHandler.php';
				$fh = new FormHandler(array('form_config' => $patch->form));
				if ($fields = call_user_func($fh->dispatch())) {
					foreach ($fields as $k => $v) {
						$patch->$k = $v;
					}
					return $this->doActionTransition('Patch', 'install', 'install');
				}
			}
			return $this->currentState;
		}

		$this->error = $this->usm->error;
		return 'InternalError';
	} // end of method goPatchInstall
	public function goPatch ($event)
	{
		assert('$this->usm->init()');

		if ($this->usm->init() && $this->usm->findPatch()) {
			assert('is_object($this->usm->patch)');
			if ($this->usm->applyPatch($this->usm->patch)) {
				return $this->doActionTransition('PostPatch', $event, $event);
			}
		}
		$this->error = $this->usm->error;
		return 'InternalError';
	} // end of method goPatch
	public function goOpenSite ($event)
	{
		$this->usm->restoreDb();
		$this->usm->removeTemporarySite();
		$this->usm->openSite();
		request_redirect('?m=about');
		die();
	} // end of method goOpenSite
	public function goPostPatch ($event)
	{
		// показать юзеру рабочий сайт.
		if ($this->usm->findPatch() && $this->usm->postPatch()) {
			return $this->doActionTransition1('ShowSite', 'refresh', $event);
		}
		$this->error = $this->usm->error;
		return 'InternalError';
	} // end of method goPostPatch
	public function goShowSite ($event)
	{
		if (file_exists($this->usm->tmp_www_dir.'index.php')) {
			global $HTML;
			$HTML = 'plain.html';
			return $this->currentState;
		}
		$this->error = 'Temporary site copy not found';
		return 'InternalError';
	} // end of method goShowSite
	public function goInstall ($event)
	{
		// скопировать файлы в директорию сайта.
		if (
			$this->usm->commitSite()
			// удалить бекап БД
			// удалить спец папку
			&& $this->usm->removeTemporarySite()
				// очистить кеш в рабочей копии
				&& $this->usm->cleanSiteCache()
					// открыть сайт
					&& $this->usm->openSite()) {
						return $this->doActionTransition1('Inactive', 'refresh', $event);
					}
		$this->error = 'Temporary site copy not found';
		return 'InternalError';
	} // end of method goInstall
}

class UpdateSiteManipulator
{
	var $ready = false;
	var $error = NULL;


	/**
	 * base_dir путь к сайту.
	 *
	 * @var string
	 * @access public
	 */
	var $base_dir = NULL;

	public function __construct ($config=array(), $init=true)
	{
		$this->config = $config;
		if ($init) $this->init();
	} // end of __construct
	public function init ($force=false)
	{
		if ($this->ready && !$force) return $this->ready;
		return $this->ready = $this->_init();

	} // end of method init
	public function _init ()
	{
		foreach ($this->config as $k=>$v) {
			switch ($k) {
			default: $this->$k = $v;
			}
		}
		// если нет дириктории, создадим рекурсивно
		if (!is_dir($this->tmp_dir)) {
			if (!@self::mkdir($this->tmp_dir, 0755, true)) {
				$this->error = "Can't create temporary directory [{$this->tmp_dir}]";
				return false;
			}
		}

		// если нет дириктории, создадим рекурсивно
		if (!is_dir($this->tmp_www_dir)) {
			if (!@self::mkdir($this->tmp_www_dir, 0755, true)) {
				$this->error = "Can't create temporary directory [{$this->tmp_www_dir}]";
				return false;
			}
		}
		if (!is_writeable($this->tmp_www_dir)) {
			$this->error = "Not writeable {$this->tmp_www_dir}";
			return false;
		}
		$this->tmp_www_url = url_dirname(request_get_base_path()).'modules/update/htdocs/index.php/';

		$this->index_php = $this->base_dir.'index.php';
		$this->index_php_bak = $this->tmp_dir.'index.php';
		if (!is_writeable($this->index_php)) {
			$this->error = "Not writeable {$this->index_php}";
			return false;
		}
		$this->etc_config_php = $this->base_dir . 'etc/config.php';

		$this->bm = new BackupManager(array('root_dir' => $this->localstate_dir.'backup/'));

		if (isset($_SESSION['backup_db'])) {
			$this->backup_db    = $this->bm->buildBackupFromFile($_SESSION['backup_db']);
		}
		if (!isset($this->backup_db)) {
			$this->backup_db    = $this->bm->createBackup();
			$_SESSION['backup_db'] = $this->backup_db->filename;
		}

		return true;
	} // end of method _init
	/**
	 * Скопировать сайт в отдельную директорию.
	 *
	 * @access public
	 * @return boolean
	 */
	public function getCurrentSiteVersion ()
	{
		assert('file_exists($this->etc_config_php)');

		require $this->etc_config_php;
		if (!isset($self['version'])) {
			$self['version'] = '1.0';
		}
		return $self['version'];
	} // end of method getCurrentSiteVersion

	var $patch = NULL;
	public function findPatch ()
	{
		if (isset($this->patch)) return true;
		// м.б. следующие исходы:
		// false -- ошибка
		// 0 все патчи установлены
		// 1 патчей нет вообще
		// 2 найден неустановленный патч
		// 3 найден неустановленный патч, но есть недостающие.

		$this->patch = NULL;
		if (!is_dir($this->updates_dir)) return true;

		assert('is_dir($this->updates_dir)');
		$d = dir($this->updates_dir);
		$patch_dirs = array();
		$versions = array();
		while (FALSE !== ($entry = $d->read())) {
			if ($entry == '.' || $entry == '..') continue;
			$p = $this->updates_dir . $entry;
			if (is_dir($p)) {
				$patch_dirs[] = $p . '/';
				$versions[] = $entry;
			}
		}
		uasort($versions, 'version_compare');

		// посмотрим текущую версию сайта в конфиге
		$currentVersion = $this->getCurrentSiteVersion();

		// найдем первый патч с большей версией
		$patch = NULL;
		foreach ($versions as $k=>$v) {
			if (version_compare($currentVersion, $v) < 0) {
				$patch = new UpdatePatch(array('patch_dir' => $patch_dirs[$k], 'site_dir' => $this->tmp_www_dir, 'base_dir' => $this->base_dir));
				if ($patch->error) {
					$this->error = $patch->error;
					return false;
				}
				// посмотрим конфиг патча.
				// в качестве базовой версии, патч может принимать какую-то одну версию,
				// или же несколько версий (полезно, если между мажорными версиями вышел ряд багфиксов).
				// т.е. можем обновлять так:
				// (1.0)   -> 1.0.1
				// (1.0.1) -> 1.0.2
				// (1.0, 1.0.1, 1.0.2) -> 1.1
				// для этого будем рассматривать $patch->fromVersion как список версий.
				$fromVersionList = is_array($patch->fromVersion) ? $patch->fromVersion : array($patch->fromVersion);

				// если текущая версия сайта содержится в списке версий,
				// значит мы нашли нужный патч!
				if (in_array($currentVersion, $fromVersionList)) {
					break;
				}
				// это не тот патч, который нам нужен.
				$patch = NULL;
			}
		}
		$this->patch = $patch;

		return true;
	} // end of method findPatch

	public function closeSite ()
	{
		if (!$this->ready || $this->error) return false;

		assert('isset($this->index_php_bak) && isset($this->index_php) && file_exists($this->index_php)');

		$php_source = file_get_contents($this->index_php);
		if (strpos($php_source, '503 Service Unavailable') === false) {
			if (!file_put_contents($this->index_php_bak, $php_source)) {
				$this->error = "Ошибка сохранения index.php во временный файл {$this->index_php_bak}";
				return false;
			}

			$new_source = '<'.'?php'.
				'
				$status = "503 Service Unavailable";
			if (substr(php_sapi_name(), 0, 3) == "cgi") header("Status: " . $status, TRUE);
			else header($_SERVER["SERVER_PROTOCOL"] . " " . $status);
			header("Retry-After: 300");
			header("X-Powered-By: HTML4.1");
			header("Content-type: text/html; charset=UTF-8");
			'
				.'?'.'>';
			$new_source .= tpl_engine()->parse('maintain.html');
			file_put_contents($this->index_php, $new_source);
		}
		return true;
	} // end of method closeSite
	public function openSite ()
	{
		if (!$this->ready || $this->error) return false;

		if (file_exists($this->index_php_bak)) {
			assert('isset($this->index_php_bak) && isset($this->index_php) && file_exists($this->index_php_bak)');
			file_put_contents($this->index_php, file_get_contents($this->index_php_bak));
			unlink($this->index_php_bak);
		}
		return true;
	} // end of method openSite

	public function restoreDb ()
	{
		// обновление структуры БД
		$dump_sql = $this->getBackup()->getPath();
		debug("Restore $dump_sql");
		if (file_exists($dump_sql)) {

			// в БД могли появиться таблицы, отсутствующие в дампе
			// удалим все таблицы из БД
			$db = $this->getMysq();
			$prefix = $db->db_prefix;
			$sql = " SHOW TABLES ";
			$rs = $db->query($sql);
			while ($row = $db->getOne($rs)) {
				reset($row);
				$table = current($row);
				if (strpos($table, $prefix) === 0) { // считаем, что названия таблиц,
					                                   // которыми мы можем манипулировать, начинается на $db->db_prefix
					if (!$db->query(" DROP TABLE `{$table}` ")) {
						$this->error = $db->error;
						return false;
					}
				}
			}

			// восстановим дамп
			unset ($_SESSION['backup_db']);
			require 'MysqlImport.php';
			$m = new MysqlImport(array('import_file' => $dump_sql, 'db' => $db));
			return $m->process();
		} else {
			$this->error = "Database dump file [{$dump_sql}] not found";
			return false;
		}
	} // end of method restoreDb
	public function getMysq ()
	{
		if (!isset($this->mysql)) {
			$config_php = $this->base_dir.'etc/conf.d/db.php';
			include ($config_php);
			require_once 'UpMysql.php';
			$this->mysql = new UpMysql($config);
		}
		return $this->mysql;
	} // end of method getMysq
	public function applyPatch ($patch)
	{
		assert('file_exists($patch->index_php)');
		$this->error = NULL;

		require_once $patch->index_php;
		if (function_exists('apply_patch')) {
			$status = apply_patch($patch);
			if (!$status) {
				$this->error = $patch->error;
			}
		} else {
			$status = true;
		}
		return $status;
	} // end of method applyPatch
	public function copySite ()
	{
		self::rm($this->tmp_www_dir, false);
		return $this->copyEntries($this->root_entries, $this->base_dir, $this->tmp_www_dir);
	} // end of method copySite
	public function copyEntries ($entries, $from, $to)
	{
		foreach ($entries as $entry) {
			self::find($from, $entry, NULL, $list);
		}
		foreach ($list as $v) {
			$source = $from.$v;
			if (is_dir($source)) {
				self::mkdir($to.$v);
			} else {
				copy($source, $to.$v);
			}
		}
		return true;
	} // end of method copyDirs
	public function removeTemporarySite ()
	{
		self::rm($this->tmp_www_dir, false);
		return true;
	} // end of method removeTemporarySite
	public function cleanSiteCache ()
	{
		self::rm($this->base_dir . 'var/cache', false);
		return true;
	} // end of method cleanSiteCache
	public function applyPatchFilesTemporarySite ()
	{
		if ($this->findPatch()) {
			if ($lines = @file($this->patch->files_txt)) {
				$this->applyChangeFiles($this->patch->files_dir, $lines, $this->tmp_www_dir);
			}
			return true;
		}
		return false;
	} // end of method applyPatchFilesTemporarySite
	public function applyChangeFiles ($src, $lines, $dst)
	{
		foreach ($lines as $line) {
			if (preg_match('#^getting\s+(.*)$#', $line, $ms)) {
				$f = $ms[1];
				self::cp($src.$f, $dst.$f);
			} elseif (preg_match('#^removing\s+(.*)$#', $line, $ms)) {
				$f = $ms[1];
				self::rm($dst.$f);
			}
		}
		return true;
	} // end of method applyChangeFiles
	public function postPatch ()
	{
		// обновить версию сайта в конфиге до текущей.
		$this->findPatch();
		require_once 'ConfigPhp.php';
		$cfg = new ConfigPhp(array('source'=>$this->tmp_www_dir.'etc/config.php'));
		$cfg->setParam('version', $this->patch->toVersion);
		unset($cfg);

		// хакнем конфиги копии сайта в tpl_www_dir, дабы заработал

		// etc/config.php : base_path
		// put site path, ending by /index.php/
		$config_php = $this->tmp_www_dir.'etc/config.php';
		copy ($config_php, $this->tmp_www_dir.'etc/config.php.bak');
		$s = file_get_contents($config_php);
		$s .= '$self["base_path"] = "'.$this->tmp_www_url.'"'.";\n";
		file_put_contents($config_php, $s);

		// etc/conf.d/tpl.php : tpl_root_href
		// omit /index.php/ in the links, generated by {{!connect}}
		$tpl_php = $this->tmp_www_dir.'etc/conf.d/tpl.php';
		copy ($tpl_php, $this->tmp_www_dir.'etc/conf.d/tpl.php.bak');
		$s = file_get_contents($tpl_php);
		$s .= "\$config['tpl_base_path'] = url_dirname(\$ctx->rh->base_path);\n";
		$s .= "\$config['tpl_data_dir'] = '$this->tmp_www_dir'.'data/';\n";
		file_put_contents($tpl_php, $s);

		return true;
	} // end of method postPatch
	public function applyPatchFiles()
	{
		if ($this->findPatch()) {
			$lines = @file($this->patch->files_txt); // маловероятно, но файла может и не быть
			                                         // (например, если патч обновляет лишь DB).
			if (!$lines) $lines = array();
			// мы должны обновлять конфиг сайта в любом случае, т.к. внутри меняем версию.
			// хотя в списке патченных файлов его никогда не будет.
			$lines[] = 'getting etc/config.php';
			$this->applyChangeFiles($this->tmp_www_dir, $lines, $this->base_dir);
			return true;
		}
		return false;
	} // end of method applyPatchFiles
	public function commitSite ()
	{
		// вернуть на место конфиги, хакнутые в postPatch
		copy ($this->tmp_www_dir.'etc/config.php.bak', $this->tmp_www_dir.'etc/config.php');
		copy ($this->tmp_www_dir.'etc/conf.d/tpl.php.bak', $this->tmp_www_dir.'etc/conf.d/tpl.php');
		unlink($this->tmp_www_dir.'etc/config.php.bak');
		unlink($this->tmp_www_dir.'etc/conf.d/tpl.php.bak');

		// обновить файлы в рабочей копии.
		$this->applyPatchFiles();
		//$this->copyEntries($this->root_entries, $this->tmp_www_dir, $this->base_dir);

		return true;
	} // end of method commitSite

	public function deleteBackup ()
	{
		$this->bm->deleteBackup($this->backup_db);
		return $this;
	} // end of method deleteBackup
	public function getBackup ()
	{
		return $this->backup_db;
	} // end of method getBackup

	static public function mkdir (/* args to php mkdir function */)
	{
		$args = func_get_args();
    $args[0] = rtrim($args[0], '/'); // HACK : 2008-06-05 : lucky
                                     // fix bug on freebsd:
                                     // it refuses directory creation if target ends by '/'
                                     // (for example '/bin/')
		return call_user_func_array('mkdir', $args);
	} // end of method mkdir
	static public function cp ($source, $target)
	{
		if (is_dir($source)) {
			@self::mkdir($target, 0777, true);
			$d = dir($source);
			while (FALSE !== ($entry = $d->read())) {
				if ($entry == '.' || $entry == '..') continue;
				self::cp($source . DIRECTORY_SEPARATOR . $entry,
					$target . DIRECTORY_SEPARATOR . $entry );
			}
			$d->close();
		} else {
			if (!@copy($source, $target)) {
				// вероятно директория, куда копируется файл не существует.
				// попробуем создать
				$d = dirname($target);
				if (!is_dir($d)) self::mkdir($d, 0777, true);
				copy($source, $target);
			}
		}
	}
	static public function rm ($source, $remove_self=true)
	{
		if (is_dir($source)) {
			$d = dir($source);
			while (FALSE !== ($entry = $d->read())) {
				if ($entry == '.' || $entry == '..') continue;
				self::rm($source . DIRECTORY_SEPARATOR . $entry, true);
			}
			$d->close();
			if ($remove_self) rmdir($source);
		} else {
			unlink($source);
		}
	}
	static public function find ($base, $source, $filter=NULL, &$list=array())
	{
		self::_find($base, $source, $filter, $list);
		return $list;
	}
	static public function _find ($base, $source, $filter=NULL, &$list=array())
	{
		if (is_dir($base.$source)) {
			$list[] = $source.'/';
			$d = dir($base.$source);
			while (FALSE !== ($entry = $d->read())) {
				if ($entry == '.' || $entry == '..') continue;
				self::_find($base, $source . DIRECTORY_SEPARATOR . $entry, $filter, $list);
			}
			$d->close();
		} else {
			$list[] = $source;
		}
	}

}

class UpdatePatch
{
	var $error = NULL;
	var $ready = false;

	/**
	 * patch_dir путь директории, где лежит патч.
	 *
	 * @var string
	 * @access public
	 */
	var $patch_dir = NULL;
	/**
	 * config_php путь конфигурационного файла патча.
	 *
	 * @var mixed
	 * @access public
	 */
	var $config_php = NULL;

	/**
	 * site_dir путь к директории где находится временная копия сайта.
	 *
	 * @var mixed
	 * @access public
	 */
	var $site_dir = NULL;
	/**
	 * base_dir путь к директории где находится рабочая копия сайта.
	 *
	 * @var mixed
	 * @access public
	 */
	var $base_dir = NULL;

	var $handler = NULL;
	public function __construct ($config=array(), $init=true)
	{
		$this->config = $config;
		if ($init) $this->init();
	}
	public function init($force=false)
	{
		if ($this->ready && !$force) return $this->ready;
		foreach ($this->config as $k=>$v) {
			$this->$k = $v;
		}
		// конфигурационный файл сайта
		if (!isset($this->config_php)) {
			$this->config_php = $this->patch_dir . 'config.php';
		}
		if (!file_exists($this->config_php)) {
			$this->error = "Patch configuration file [{$this->config_php}] not exists";
			return false;
		}

		// грузим данные из конфигурационного файла патча
		require $this->config_php;
		foreach ($config as $k=>$v) {
			$this->$k = $v;
		}

		// хендлер патча
		if (!isset($this->index_php)) {
			$this->index_php = $this->patch_dir . 'index.php';
		}
		if (!file_exists($this->index_php)) {
			$this->error = "Patch handler file [{$this->index_php}] not exists";
			return false;
		}

		if (!isset($this->files_txt)) {
			$this->files_txt = $this->patch_dir . 'files.txt';
		}

		if (!isset($this->files_dir)) {
			$this->files_dir = $this->patch_dir . 'files/';
		}

		if (isset($this->form)) {
			$this->form['patch'] = $this;
		}

		return $this->ready = $this->_init();
	}
	public function _init()
	{
		if ($lines = @file($this->files_txt)) {
			$e = '';
			foreach ($lines as $line) {
				if (preg_match('#^getting\s+(.*)$#', $line, $ms)) {
					$f = $ms[1];
				} elseif (preg_match('#^removing\s+(.*)$#', $line, $ms)) {
					$f = $ms[1];
				}
				$dst = $this->base_dir.$f;
				if (file_exists($dst) && !is_writeable($dst)) $e .= $f."\n ";
			}
			if ($e) { $this->error = "Can't write to files: $e"; }
		}
		return !$this->error;
	}
} // end of class UpdatePatch

